package sfsumapserver;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
 * This class reads and processes the request, then creates and sends the response.
 * 
 * @author Terry Wong
 * @modified Benjamin Britten
 */
public class Process {

	private final Socket client;
	private int index, totalPoints = 0, winPoints;
	
	private String requestline, playerID, goalName, currentLocation, goalLocation,
			option, password, tool, targetPlayer, indicator,
					currentEffect, clue;
	
	private SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
	private float distance;
	private long startTime, currentTime, elapsedTime, bonusTime;
	private PlayerStats player;
	
	private boolean authenticationFailure = false;
	
	private static HashMap<String, Float> topThreeTeams = new HashMap<String, Float>();
	private static HashMap<String, String> topThreeClues = new HashMap<String, String>();
	public static HashMap<String, Integer> topScoreTeams = new HashMap<String, Integer>();
	public static HashMap<String, Long> topTimeTeams = new HashMap<String, Long>();
	
	private PlayerLog loggedPlayer;
	private Authentication authen;
	private boolean toolSuccess;
	private String message;

	
	/**
	 * Default constructor used to reset variables and data structures for
	 * each new incoming request.
	 * 
	 * @modified Benjamin Britten
	 */
	public Process(Socket client) {
		this.client = client;
		dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
		targetPlayer = null;
		toolSuccess = false;
		message = null;
		indicator = "";
		clue = "";
		currentEffect = "";
		password = "";
		tool = "";
		bonusTime = 600;
		winPoints = 500;
	}

	
	/**
	 * This function is used by the Process object to read the request message and parse the JSON sent in
	 * the body of the POST request sent by the client for later use when processing the request.
	 * 
	 * @author Benjamin Britten
	 */
	public void readRequest() throws IOException {
		
		BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
		requestline = in.readLine();
		
		/* Reads through the headers in the POST request until it reaches the empty line that
		 * separates the headers from the body of the request
		 */
		while (!(requestline.isEmpty())) {
			// Debug
			System.out.println("requestline : " + requestline);		
			requestline = in.readLine();		
		}		
		
		requestline = in.readLine();
		
		System.out.println("requestline : " + requestline);

		JsonObject jsonReceived = new JsonParser().parse(requestline).getAsJsonObject();

		if (jsonReceived.has("playerID")) {
			playerID = jsonReceived.getAsJsonPrimitive("playerID").getAsString();
		}

		if (jsonReceived.has("currentLocation")) {
			currentLocation = jsonReceived.getAsJsonPrimitive("currentLocation").getAsString();
		}

		if (jsonReceived.has("option")) {
			option = jsonReceived.getAsJsonPrimitive("option").getAsString();
		}

		if (jsonReceived.has("password")) {
			password = jsonReceived.getAsJsonPrimitive("password").getAsString();
		}

		if (jsonReceived.has("tool")) {
			tool = jsonReceived.getAsJsonPrimitive("tool").getAsString();
		}

		if (jsonReceived.has("targetPlayer")) {
			targetPlayer = jsonReceived.getAsJsonPrimitive("targetPlayer").getAsString();
		}
		else {
			targetPlayer = playerID;
		}
		
		if(jsonReceived.has("message")) {
			message = jsonReceived.getAsJsonPrimitive("message").getAsString();
		}
	}

	
	/**
	 * This function process each specific request based on the type of request, which
	 * is represented by the value sent with the "option" key in the JSON.
	 * 
	 * @modified Benjamin Britten
	 */
	public synchronized void processRequest() throws IOException {
		
		// Handles the "signIn" option, creating a new account if you are not an existing player
		if (option.equalsIgnoreCase("signIn")) {
			if (!(ServerTable.playerInfoContains(playerID))) {
				loggedPlayer = new PlayerLog(playerID, password, "5000");
				loggedPlayer.add();
				ServerTable.setPlayerInfo(playerID);
				ServerTable.getPlayerInfo(playerID).setPlayerPoints(5000);
			} else {
				authen = new Authentication(playerID, password);
				String ID = authen.checkAuth();
				System.out.println("signIn response: " + ID);
				if (ID == null || !ServerTable.playerInfoContains(playerID)) {
					authenticationFailure = true;
				}
			}
			player = ServerTable.getPlayerInfo(playerID);
			
		/* Handles the "getClue" option, sends a clue to the current goal plus checks and
		 * sends the players "indicator" status in reaching the goal
		 */
		} else if (option.equalsIgnoreCase("getClue")) {
			player = ServerTable.getPlayerInfo(playerID);
			goalName = ServerTable.getGoal();			
			System.out.println("goalName: " + goalName);
			if (player.getGoal().equals("")) {
				player.setGoal(goalName);
				player.setClue();
				startTime = System.currentTimeMillis();
				System.out.println("player startTime: " + startTime);
				player.setStartTime(startTime);
			}
			goalLocation = ServerTable.getGoalLocation();
			computeDistance();
			computeElapsedTime();
			player.setDistance(distance);
			clue = player.getClue();
			setIndicator();
			//updateTopThree();
			
		/* Handles the "setTool" option, attempts to purchase the tool indicated by the "tool" key
		 * in the JSON plus checks and sends the players "indicator" status in reaching the goal.
		 */
		} else if (option.equalsIgnoreCase("setTool")) {
			player = ServerTable.getPlayerInfo(playerID);
			goalName = ServerTable.getGoal();
			System.out.println("goalName: " + goalName);
			if (player.getGoal().equals("")) {
				player.setGoal(goalName);
				player.setClue();
				startTime = System.currentTimeMillis();
				System.out.println("player startTime: " + startTime);
				player.setStartTime(startTime);
			}
			// Condition for "steal" tool, checks for existing steal
			if (tool.equals("stealer")) {
				if(!(ServerTable.isStealer())) {
					ServerTable.setStealer(playerID);
					toolSuccess = true;
				}
			// Condition for "lock-out" tool, checks for existing lock-out
			} else if (tool.equals("lockout")) {
				if (!(ServerTable.isLockOut())) {
					ServerTable.setLockOut();
					toolSuccess = true;
				}
			} else {
				/* Check for not "compass" tool, compass is handled on android client.
				 * Not compass tools are "dizzy", "smokeBomb", "clear", and "taunt"
				 * "taunt" is the only tool that sends a non-null "message" value
				 */
				if (!(tool.equals("compass"))) {
					toolSuccess = ServerTable.getPlayerInfo(targetPlayer).activateTool(tool,message);
				}
				else
					toolSuccess = true;
			}
			
			// Used in setIndicator() to decrease players points for cost of tool if purchase successful
			if(toolSuccess == true) {
				totalPoints -= ServerTable.getToolPrice(tool);
			}
			
			// Future Use. Not dealing damage to other players in this version of game
			// ServerTable.getPlayerInfo(targetPlayer).setPlayerPoints(ServerTable.getToolDamage(tool));
			
			// Various calculations and setting of values for use in response
			goalLocation = ServerTable.getGoalLocation();
			computeDistance();
			computeElapsedTime();
			player.setDistance(distance);
			setIndicator();
			updateTopThree();
		}
	}


	/**
	 * function to compute the distance between player's current position and
	 * the goal position.
	 */
	private void computeDistance() {
		index = currentLocation.indexOf(",");
		float longitude1 = Float.valueOf(currentLocation.substring(0, index));
		float latitude1 = Float.valueOf(currentLocation.substring(index + 1));
		index = goalLocation.indexOf(",");
		float longitude2 = Float.valueOf(goalLocation.substring(0, index));
		float latitude2 = Float.valueOf(goalLocation.substring(index + 1));
		distance = (float) Math.sqrt(Math.pow(longitude1 - longitude2, 2)
				+ Math.pow(latitude1 - latitude2, 2));
		System.out.println("distance: " + distance + " degrees");
	}

	/**
	 * function to compute the player's elapsed time.
	 */
	private long computeElapsedTime() {
		currentTime = System.currentTimeMillis();
		if (player.getStartTime() != 0) {
			elapsedTime = currentTime - Long.valueOf(player.getStartTime());
		} else {
			elapsedTime = 0;
		}
		return elapsedTime;
	}

	/**
	 * function to update the top three teams closest to the goal
	 */
	private synchronized void updateTopThree() {
		if (indicator.equals("Win")) {
			for (Map.Entry<String, Float> entry : topThreeTeams.entrySet()) {
				topThreeTeams.remove(entry.getKey());
				topThreeClues.remove(entry.getKey());
			}
		} else if (topThreeTeams.size() <= 3) {
			topThreeTeams.put(playerID, distance);
			topThreeClues.put(playerID, indicator);
		} else {
			String key = "";
			Float max = Float.valueOf(Float.MIN_VALUE);
			for (Map.Entry<String, Float> entry : topThreeTeams.entrySet()) {
				if (max.compareTo(entry.getValue()) < 0) {
					key = entry.getKey();
					max = entry.getValue();
				}
			}
			if (distance < max && !topThreeTeams.containsKey(playerID)) {
				topThreeTeams.put(playerID, distance);
				topThreeClues.put(playerID, indicator);
				topThreeTeams.remove(key);
			} else if (distance < max && topThreeTeams.containsKey(playerID)) {
				topThreeTeams.put(playerID, distance);
				topThreeClues.put(playerID, indicator);
			}
		}
	}

	/**
	 * function to update the top score teams
	 */
	private synchronized void updateTopScoreTeams() {
		if (topScoreTeams.size() <= 5) {
			topScoreTeams.put(playerID, player.getPlayerPoints());
		} else {
			String key = "";
			Integer min = Integer.valueOf(Integer.MAX_VALUE);
			for (Map.Entry<String, Integer> entry : topScoreTeams.entrySet()) {
				if (min.compareTo(entry.getValue()) > 0) {
					key = entry.getKey();
					min = entry.getValue();
				}
			}
			if (player.getPlayerPoints() > min) {
				topScoreTeams.remove(key);
				topScoreTeams.put(playerID, player.getPlayerPoints());
			}
		}
	}

	/**
	 * function to update the top time ranked teams
	 */
	private synchronized void updateTopTimeTeams() {
		if (topTimeTeams.size() <= 5) {
			topTimeTeams.put(playerID, elapsedTime / 1000);
		} else {
			String key = "";
			Long max = Long.valueOf(Long.MIN_VALUE);
			for (Map.Entry<String, Long> entry : topTimeTeams.entrySet()) {
				if (max.compareTo(entry.getValue()) < 0) {
					key = entry.getKey();
					max = entry.getValue();
				}
			}
			if (elapsedTime / 1000 < max) {
				topTimeTeams.remove(key);
				topTimeTeams.put(playerID, elapsedTime / 1000);
			}
		}
	}

	/**
	 * This function generates the "indicator" : WIN, HOT, WARM, COLD, SMOKE, STEAL
	 * 
	 * @modified Benjamin Britten
	 */
	private synchronized void setIndicator() {
		
		if (player.checkStolenWin()) {
			
			// you stole the win!
			indicator = "Win";
			totalPoints += winPoints;
			player.resetStolenWin();
			updateTopTimeTeams();
		
		} else {
			
			if (!player.getCurrentEffect().equals("")) {
				currentEffect = player.getCurrentEffect();
			}
			
			if (distance <= 0.001) {
				
				// Checks ServerTable to see if a stealer is present and it's not you
				if (ServerTable.isStealer()) {
					String stealerName = ServerTable.getStealerName();
					if (stealerName.equals(playerID)) {
						indicator = "Win";
					} else {
						indicator = "steal";
						ServerTable.getPlayerInfo(ServerTable.getStealerName()).setStolenWin();
					}
					ServerTable.removeGoal();
					ServerTable.resetGame();
					
				// Checks ServerTable to see if a lock-out is present
				} else if (ServerTable.isLockOut()) {
					indicator = "lock-out";
				} else {
					indicator = "Win";
					totalPoints += winPoints;
					// Bonus for reaching goal within a set amount of time
					if (elapsedTime <= bonusTime) {
						totalPoints += winPoints;
					}
					updateTopTimeTeams();
					ServerTable.removeGoal();
					ServerTable.resetGame();
				}
			
			} else if (distance <= 0.003) {
				indicator = "Hot";
				if (!player.checkHotOnce()) {
					totalPoints += 200;
					player.setHotOnce();
				}
			
			} else if (distance <= 0.006) {
				indicator = "Warm";
				if (!player.checkWarmOnce()) {
					totalPoints += 100;
					player.setWarmOnce();
				}
			
			} else {
				indicator = "Cold";
			}
			
			if (!indicator.equals("Win")) {
				if (currentEffect.equals("smokeBomb")) {
					distance = 1000000;
					indicator = "smokeBomb";
				} else if (currentEffect.equals("dizzyMonkey")) {
					distance = (float) 0.00200;
					indicator = "Cold";
				}
			}
		}
		
		if (player.checkTaunt()) {
			clue = player.getTaunt();
		}
		
		player.setIndicator(indicator);
		player.setPlayerPoints(totalPoints);
		updateTopScoreTeams();
	}

	/**
	 * This function generates the response and sends it to the Android client.
	 * 
	 * @modified Benjamin Britten
	 */
	public void writeResponse() throws IOException {
		BufferedOutputStream out = new BufferedOutputStream(
				client.getOutputStream());
		Date date = new Date();
		PrintWriter writer = new PrintWriter(out, true);
		writer.println("HTTP/1.1 200 OK");
		writer.println("Server: SFSUMapServer");
		writer.println("Date: " + dateFormat.format(date));
		writer.println("Content-Type: text/plain");
		writer.println("Connection: close");
		String output = "{";
		if (option.equalsIgnoreCase("signIn")) {
			output += "\"signIn\":\"";
			if (!authenticationFailure) {
				output += "Good\"";
				output += ", \"playerPoints\":\"" + player.getPlayerPoints();
			} else {
				output += "Bad";
			}
			output += "\"";
		} else if (option.equalsIgnoreCase("getClue")) {
			output += "\"clue\":\"" + clue + "\"";
			output += ", \"distance\":\"" + distance + "\"";
			output += ", \"goalLocation\":\"" + goalLocation + "\"";
			output += ", \"indicator\":\"" + indicator + "\"";

			// for elapsed time
			output += ", \"elapsedTime\":\""
					+ dateFormat.format(new Date(elapsedTime)) + "\"";

			output += ", \"playerPoints\":\"" + player.getPlayerPoints() + "\"";
		} else if (option.equalsIgnoreCase("setTool")) {
			output += "\"status\":\"";
			if(toolSuccess)
				output += "OK\"";
			else
				output += "BAD\"";
			
			output += ", \"tool\":\"" + tool + "\"";
			output += ", \"distance\":\"" + distance + "\"";
			output += ", \"goalLocation\":\"" + goalLocation + "\"";

			// for elapsed time
			output += ", \"elapsedTime\":\""
					+ dateFormat.format(new Date(elapsedTime)) + "\"";

			output += ", \"playerPoints\":\"" + player.getPlayerPoints() + "\"";
			output += ", \"targetPlayer\":\"" + targetPlayer + "\"";
		} else if (option.equalsIgnoreCase("getTopThree")) {
			// get each top 3 team's clue and distance by using keywords
			// "TopTeam1",
			// "TopTeam2", "TopTeam3"
			HashMap<String, Float> tmp = new HashMap<String, Float>();
			tmp.putAll(topThreeTeams);
			String key = "";
			Float min = Float.valueOf(Integer.MAX_VALUE);
			for (int i = 0; i < 3; i++) {
				for (Map.Entry<String, Float> entry : tmp.entrySet()) {
					if (min.compareTo(entry.getValue()) > 0) {
						key = entry.getKey();
						min = entry.getValue();
					}
				}
				if (tmp.get(key) != null) {
					output += "\"TopTeam" + (i + 1) + "\":\"" + key + " "
							+ topThreeClues.get(key) + " " + tmp.get(key)
							+ "\",";
					tmp.remove(key);
				}
			}
		} else if (option.equalsIgnoreCase("getTopScores")) {
			// get each top 5 team's top score by using keywords "TopTeam1",
			// "TopTeam2", "TopTeam3", etc.
			HashMap<String, Integer> tmp = new HashMap<String, Integer>();
			tmp.putAll(topScoreTeams);
			String key = "";
			Integer max = Integer.valueOf(Integer.MIN_VALUE);
			for (int i = 0; i < 5; i++) {
				for (Map.Entry<String, Integer> entry : tmp.entrySet()) {
					if (max.compareTo(entry.getValue()) < 0) {
						key = entry.getKey();
						max = entry.getValue();
					}
				}
				if (tmp.get(key) != null) {
					output += "\"TopTeam" + (i + 1) + "\":\"" + key + ": "
							+ tmp.get(key) + " points\", ";
					tmp.remove(key);
				}
			}
		} else if (option.equalsIgnoreCase("getTopTime")) {
			// get each top 5 team's top time by using keywords "TopTeam1",
			// "TopTeam2", "TopTeam3", etc.
			HashMap<String, Long> tmp = new HashMap<String, Long>();
			tmp.putAll(topTimeTeams);
			String key = "";
			Long min = Long.valueOf(Long.MAX_VALUE);
			for (int i = 0; i < 5; i++) {
				for (Map.Entry<String, Long> entry : tmp.entrySet()) {
					if (min.compareTo(entry.getValue()) > 0) {
						key = entry.getKey();
						min = entry.getValue();
					}
				}
				if (tmp.get(key) != null) {
					output += "\"TopTeam" + (i + 1) + "\":\"" + key + ": "
							+ tmp.get(key) + " seconds\", ";
					tmp.remove(key);
				}
			}
		} else if(option.equalsIgnoreCase("getPlayers")) {
			Set<String> keySet = ServerTable.getAllPlayerNames();
			String[] allPlayers = keySet.toArray(new String[0]);
			if(allPlayers.length > 0) {
				output += "\"players\":[\""+allPlayers[0];
				for(int i=1; i < allPlayers.length; i++) {
					output += "\", \"" + allPlayers[i];
				}
				output += "\"]";
			}
		}
		output += "}";
		writer.println("Content-Length: " + output.length());
		System.out.println("Content-Length: " + output.length());
		writer.println("");
		writer.println(output);
		System.out.println("\n" + output + "\n");
		writer.flush();
		writer.close();
		out.close();
	}

	/**
	 * function to record all the stats to log files
	 */
	public synchronized void writeToLog() throws Exception {
		try {
			player = ServerTable.getPlayerInfo(playerID);
			loggedPlayer = new PlayerLog(playerID);
			System.out.println("player points: " + player.getPlayerPoints());
			loggedPlayer.update(String.valueOf(player.getPlayerPoints()));
			String log = "";
			for (Map.Entry<String, Integer> entry : topScoreTeams.entrySet()) {
				log += entry.getKey() + ":" + entry.getValue() + " ";
			}
			BufferedWriter bw = new BufferedWriter(new FileWriter(new File(
					"score rankings.log")));
			bw.write(log);
			bw.flush();
			bw.close();
			if (indicator.equals("Win")) {
				log = "";
				for (Map.Entry<String, Long> entry : topTimeTeams.entrySet()) {
					log += entry.getKey() + ":" + entry.getValue() + " ";
				}
				bw = new BufferedWriter(new FileWriter(new File(
						"time rankings.log")));
				bw.write(log);
				bw.flush();
				bw.close();
			}
		} catch (IOException e) { // if there is an error in reading the file
			System.err.println(e.getMessage());
		}
	}
}